/*
 * 
 * 
 * 
 * 
 * 
 * the License.  
 *
 *    
 *
 * 
 * 
 * 
 * 
 * 
 */

package drds.server.memory.unsafe.types;

import java.io.Serializable;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * The internal representation of interval type.
 */
public final class CalendarInterval implements Serializable {
	public static final long MICROS_PER_MILLI = 1000L;
	public static final long MICROS_PER_SECOND = MICROS_PER_MILLI * 1000;
	public static final long MICROS_PER_MINUTE = MICROS_PER_SECOND * 60;
	public static final long MICROS_PER_HOUR = MICROS_PER_MINUTE * 60;
	public static final long MICROS_PER_DAY = MICROS_PER_HOUR * 24;
	public static final long MICROS_PER_WEEK = MICROS_PER_DAY * 7;

	/**
	 * A function to generate regex which matches interval string's unit part
	 * like "3 years".
	 * 
	 * First, we can leave out some units in interval string, and we only care
	 * about the value of unit, so here we use non-capturing group to wrap the
	 * actual regex. At the beginning of the actual regex, we should match
	 * spaces before the unit part. Next is the number part, starts with an
	 * optional "-" to represent negative value. We use capturing group to wrap
	 * this part as we need the value later. Finally is the unit name, ends with
	 * an optional "s".
	 */
	private static String unitRegex(String unit) {
		return "(?:\\s+(-?\\d+)\\s+" + unit + "s?)?";
	}

	private static Pattern p = Pattern.compile("interval" + unitRegex("year") + unitRegex("month") + unitRegex("week") + unitRegex("day") + unitRegex("hour") + unitRegex("minute") + unitRegex("second") + unitRegex("millisecond") + unitRegex("microsecond"));

	private static Pattern yearMonthPattern = Pattern.compile("^(?:['|\"])?([+|-])?(\\d+)-(\\d+)(?:['|\"])?$");

	private static Pattern dayTimePattern = Pattern.compile("^(?:['|\"])?([+|-])?(\\d+) (\\d+):(\\d+):(\\d+)(\\.(\\d+))?(?:['|\"])?$");

	private static Pattern quoteTrimPattern = Pattern.compile("^(?:['|\"])?(.*?)(?:['|\"])?$");

	private static long toLong(String s) {
		if (s == null) {
			return 0;
		} else {
			return Long.parseLong(s);
		}
	}

	public static CalendarInterval fromString(String s) {
		if (s == null) {
			return null;
		}
		s = s.trim();
		Matcher m = p.matcher(s);
		if (!m.matches() || s.equals("interval")) {
			return null;
		} else {
			long months = toLong(m.group(1)) * 12 + toLong(m.group(2));
			long microseconds = toLong(m.group(3)) * MICROS_PER_WEEK;
			microseconds += toLong(m.group(4)) * MICROS_PER_DAY;
			microseconds += toLong(m.group(5)) * MICROS_PER_HOUR;
			microseconds += toLong(m.group(6)) * MICROS_PER_MINUTE;
			microseconds += toLong(m.group(7)) * MICROS_PER_SECOND;
			microseconds += toLong(m.group(8)) * MICROS_PER_MILLI;
			microseconds += toLong(m.group(9));
			return new CalendarInterval((int) months, microseconds);
		}
	}

	public static long toLongWithRange(String fieldName, String s, long minValue, long maxValue) throws IllegalArgumentException {
		long result = 0;
		if (s != null) {
			result = Long.parseLong(s);
			if (result < minValue || result > maxValue) {
				throw new IllegalArgumentException(String.format("%s %d outside range [%d, %d]", fieldName, result, minValue, maxValue));
			}
		}
		return result;
	}

	/**
	 * Parse YearMonth string in form: [-]YYYY-MM
	 * 
	 * adapted from HiveIntervalYearMonth.valueOf
	 */
	public static CalendarInterval fromYearMonthString(String s) throws IllegalArgumentException {
		CalendarInterval result = null;
		if (s == null) {
			throw new IllegalArgumentException("Interval year-month string was null");
		}
		s = s.trim();
		Matcher m = yearMonthPattern.matcher(s);
		if (!m.matches()) {
			throw new IllegalArgumentException("Interval string does not match year-month format of 'y-m': " + s);
		} else {
			try {
				int sign = m.group(1) != null && m.group(1).equals("-") ? -1 : 1;
				int years = (int) toLongWithRange("year", m.group(2), 0, Integer.MAX_VALUE);
				int months = (int) toLongWithRange("month", m.group(3), 0, 11);
				result = new CalendarInterval(sign * (years * 12 + months), 0);
			} catch (Exception e) {
				throw new IllegalArgumentException("Error parsing interval year-month string: " + e.getMessage(), e);
			}
		}
		return result;
	}

	/**
	 * Parse dayTime string in form: [-]d HH:mm:ss.nnnnnnnnn
	 * 
	 * adapted from HiveIntervalDayTime.valueOf
	 */
	public static CalendarInterval fromDayTimeString(String s) throws IllegalArgumentException {
		CalendarInterval result = null;
		if (s == null) {
			throw new IllegalArgumentException("Interval day-time string was null");
		}
		s = s.trim();
		Matcher m = dayTimePattern.matcher(s);
		if (!m.matches()) {
			throw new IllegalArgumentException("Interval string does not match day-time format of 'd h:m:s.n': " + s);
		} else {
			try {
				int sign = m.group(1) != null && m.group(1).equals("-") ? -1 : 1;
				long days = toLongWithRange("day", m.group(2), 0, Integer.MAX_VALUE);
				long hours = toLongWithRange("hour", m.group(3), 0, 23);
				long minutes = toLongWithRange("minute", m.group(4), 0, 59);
				long seconds = toLongWithRange("second", m.group(5), 0, 59);
				// Hive allow nanosecond precision interval
				long nanos = toLongWithRange("nanosecond", m.group(7), 0L, 999999999L);
				result = new CalendarInterval(0, sign * (days * MICROS_PER_DAY + hours * MICROS_PER_HOUR + minutes * MICROS_PER_MINUTE + seconds * MICROS_PER_SECOND + nanos / 1000L));
			} catch (Exception e) {
				throw new IllegalArgumentException("Error parsing interval day-time string: " + e.getMessage(), e);
			}
		}
		return result;
	}

	public static CalendarInterval fromSingleUnitString(String unit, String s) throws IllegalArgumentException {

		CalendarInterval result = null;
		if (s == null) {
			throw new IllegalArgumentException(String.format("Interval %s string was null", unit));
		}
		s = s.trim();
		Matcher m = quoteTrimPattern.matcher(s);
		if (!m.matches()) {
			throw new IllegalArgumentException("Interval string does not match day-time format of 'd h:m:s.n': " + s);
		} else {
			try {
				if (unit.equals("year")) {
					int year = (int) toLongWithRange("year", m.group(1), Integer.MIN_VALUE / 12, Integer.MAX_VALUE / 12);
					result = new CalendarInterval(year * 12, 0L);

				} else if (unit.equals("month")) {
					int month = (int) toLongWithRange("month", m.group(1), Integer.MIN_VALUE, Integer.MAX_VALUE);
					result = new CalendarInterval(month, 0L);

				} else if (unit.equals("week")) {
					long week = toLongWithRange("week", m.group(1), Long.MIN_VALUE / MICROS_PER_WEEK, Long.MAX_VALUE / MICROS_PER_WEEK);
					result = new CalendarInterval(0, week * MICROS_PER_WEEK);

				} else if (unit.equals("day")) {
					long day = toLongWithRange("day", m.group(1), Long.MIN_VALUE / MICROS_PER_DAY, Long.MAX_VALUE / MICROS_PER_DAY);
					result = new CalendarInterval(0, day * MICROS_PER_DAY);

				} else if (unit.equals("hour")) {
					long hour = toLongWithRange("hour", m.group(1), Long.MIN_VALUE / MICROS_PER_HOUR, Long.MAX_VALUE / MICROS_PER_HOUR);
					result = new CalendarInterval(0, hour * MICROS_PER_HOUR);

				} else if (unit.equals("minute")) {
					long minute = toLongWithRange("minute", m.group(1), Long.MIN_VALUE / MICROS_PER_MINUTE, Long.MAX_VALUE / MICROS_PER_MINUTE);
					result = new CalendarInterval(0, minute * MICROS_PER_MINUTE);

				} else if (unit.equals("second")) {
					long micros = parseSecondNano(m.group(1));
					result = new CalendarInterval(0, micros);

				} else if (unit.equals("millisecond")) {
					long millisecond = toLongWithRange("millisecond", m.group(1), Long.MIN_VALUE / MICROS_PER_MILLI, Long.MAX_VALUE / MICROS_PER_MILLI);
					result = new CalendarInterval(0, millisecond * MICROS_PER_MILLI);

				} else if (unit.equals("microsecond")) {
					long micros = Long.parseLong(m.group(1));
					result = new CalendarInterval(0, micros);
				}
			} catch (Exception e) {
				throw new IllegalArgumentException("Error parsing interval string: " + e.getMessage(), e);
			}
		}
		return result;
	}

	/**
	 * Parse second_nano string in ss.nnnnnnnnn format to microseconds
	 */
	public static long parseSecondNano(String secondNano) throws IllegalArgumentException {
		String[] parts = secondNano.split("\\.");
		if (parts.length == 1) {
			return toLongWithRange("second", parts[0], Long.MIN_VALUE / MICROS_PER_SECOND, Long.MAX_VALUE / MICROS_PER_SECOND) * MICROS_PER_SECOND;

		} else if (parts.length == 2) {
			long seconds = parts[0].equals("") ? 0L : toLongWithRange("second", parts[0], Long.MIN_VALUE / MICROS_PER_SECOND, Long.MAX_VALUE / MICROS_PER_SECOND);
			long nanos = toLongWithRange("nanosecond", parts[1], 0L, 999999999L);
			return seconds * MICROS_PER_SECOND + nanos / 1000L;

		} else {
			throw new IllegalArgumentException("Interval string does not match second-nano format of ss.nnnnnnnnn");
		}
	}

	public final int months;
	public final long microseconds;

	public CalendarInterval(int months, long microseconds) {
		this.months = months;
		this.microseconds = microseconds;
	}

	public CalendarInterval add(CalendarInterval that) {
		int months = this.months + that.months;
		long microseconds = this.microseconds + that.microseconds;
		return new CalendarInterval(months, microseconds);
	}

	public CalendarInterval subtract(CalendarInterval that) {
		int months = this.months - that.months;
		long microseconds = this.microseconds - that.microseconds;
		return new CalendarInterval(months, microseconds);
	}

	public CalendarInterval negate() {
		return new CalendarInterval(-this.months, -this.microseconds);
	}

	@Override
	public boolean equals(Object other) {
		if (this == other)
			return true;
		if (other == null || !(other instanceof CalendarInterval))
			return false;

		CalendarInterval o = (CalendarInterval) other;
		return this.months == o.months && this.microseconds == o.microseconds;
	}

	@Override
	public int hashCode() {
		return 31 * months + (int) microseconds;
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder("interval");

		if (months != 0) {
			appendUnit(sb, months / 12, "year");
			appendUnit(sb, months % 12, "month");
		}

		if (microseconds != 0) {
			long rest = microseconds;
			appendUnit(sb, rest / MICROS_PER_WEEK, "week");
			rest %= MICROS_PER_WEEK;
			appendUnit(sb, rest / MICROS_PER_DAY, "day");
			rest %= MICROS_PER_DAY;
			appendUnit(sb, rest / MICROS_PER_HOUR, "hour");
			rest %= MICROS_PER_HOUR;
			appendUnit(sb, rest / MICROS_PER_MINUTE, "minute");
			rest %= MICROS_PER_MINUTE;
			appendUnit(sb, rest / MICROS_PER_SECOND, "second");
			rest %= MICROS_PER_SECOND;
			appendUnit(sb, rest / MICROS_PER_MILLI, "millisecond");
			rest %= MICROS_PER_MILLI;
			appendUnit(sb, rest, "microsecond");
		}

		return sb.toString();
	}

	private void appendUnit(StringBuilder sb, long value, String unit) {
		if (value != 0) {
			sb.append(' ').append(value).append(' ').append(unit).append('s');
		}
	}
}
